Skip to content

chore(gates): name the added modules and import paths when an eager closure grows - #2471

Merged
thymikee merged 3 commits into
mainfrom
claude/gate-growth-diagnostics
Sep 10, 2026
Merged

chore(gates): name the added modules and import paths when an eager closure grows#2471
thymikee merged 3 commits into
mainfrom
claude/gate-growth-diagnostics

Conversation

@thymikee

@thymikee thymikee commented Sep 10, 2026

Copy link
Copy Markdown
Member

What changed

The eager-closure NO-GROWTH gate (scripts/__tests__/eager-closure-budgets.ts +
.test.ts) has good bones — it already names the shortest import chain to a growth —
but its message shape sent reviewers the wrong way on #2423. This is a small
diagnostics-only change; the verdict logic (when an entry is flagged as grown) is
identical.

  1. describeClosureGrowth now lists every newly-added module (bounded to 10, with
    a count of any more), each with the shortest static import route from the entry —
    not just the first one.
  2. New describeSharedGrowthHomes, run once after every entry is evaluated: for each
    added module that two or more grown entries share, it prints one block naming
    those entries with how much each grew, and the modules exactly those entries already
    evaluate at the merge-base, scoped to the added module's own package. Labeled
    neutrally as candidate homes, not a verdict.
  3. classifyGrowth's closing advice no longer prescribes one fix. It now states the
    two common causes (a new static edge, or something that used to load lazily) and
    the two remedies (give the symbol a home in a module already in the closure, or
    make the new edge lazy), leaving the choice to the reader.

Two properties the aggregation keeps, from this PR's review round:

  • Only net growth aggregates. Entries carry their head closure size and the
    grouping applies the same condition classifyGrowth does. A closure that swapped one
    module for another, or shrank while adding one, has newly evaluated modules and no
    growth, so it never appears and never fails the aggregate.
  • One block per added module, never a union. Homes common to one group's entries say
    nothing about another group's, so groups stay separate all the way into the message
    and the label names the group ("Entries that grew by <module> …") instead of
    claiming every failing entry.

Motivated by the review rounds on #2423.

Why

#2423's growth was one new tiny constants module reached from several platform-apple
facades through runner-cache-metadata.ts. The old message named only the first
added module and always said "move it behind a dynamic import" — which sent five
reviewers toward a dynamic import for a constant, when the right fix was a home in a
module every affected facade already had in its closure. Nothing in any single
entry's own failure could show that the "new" modules across five separate CI
failures were mostly the same one.

Evidence: before / after on a real planted growth

Planted an unused import './toolchain-probe-budget.ts'; (a new, empty module) into
packages/platform-apple/src/runner/runner-cache-metadata.ts — the same shape as
#2423: one new module reached by several platform-apple facades through the same
file. Ran the gate, captured the message, then discarded the plant (git diff was
back to just the two files below before committing).

Before (one entry's failure — packages/platform-apple/src/runner/index.ts):

packages/platform-apple/src/runner/index.ts evaluates 14 modules on import; the merge-base evaluated 13. Something that used to load on demand now loads eagerly, or a new static edge was added: move it behind a function-scoped `await import`.

First newly evaluated module, by shortest import route:
  packages/platform-apple/src/runner/index.ts
      -> packages/platform-apple/src/runner/runner-cache-metadata.ts
      -> packages/platform-apple/src/runner/toolchain-probe-budget.ts

That one entry was one of seven that failed the same way — app-lifecycle-facade,
app-resolution-facade, doctor-facade, perf-facade, physical-device-facade,
runner-operations-facade, and runner/index.ts itself — each printing its own
version of the same chain, with nothing tying them together.

After (same entry):

packages/platform-apple/src/runner/index.ts evaluates 14 modules on import; the merge-base evaluated 13. That means either a new static edge was added, or something that used to load on demand now loads eagerly. The fix is either to give the new code a home in a module the closure already evaluates, or to move the new edge behind a function-scoped `await import` -- see the added module(s) below for which one fits.

Newly evaluated module(s), each by shortest import route from the entry:
  packages/platform-apple/src/runner/index.ts → packages/platform-apple/src/runner/runner-cache-metadata.ts → packages/platform-apple/src/runner/toolchain-probe-budget.ts

After, printed once for the whole run (the new aggregation, one block per added
module, re-captured after the review round):

More than one entry grew by the same new module -- see the shared homes below instead of chasing each entry's own diagnostic separately:

packages/platform-apple/src/runner/toolchain-probe-budget.ts -- newly evaluated by packages/platform-apple/src/app-lifecycle-facade.ts (+1), packages/platform-apple/src/app-resolution-facade.ts (+1), packages/platform-apple/src/doctor-facade.ts (+1), packages/platform-apple/src/perf-facade.ts (+1), packages/platform-apple/src/physical-device-facade.ts (+1), packages/platform-apple/src/runner-operations-facade.ts (+1), packages/platform-apple/src/runner/index.ts (+1)
Entries that grew by packages/platform-apple/src/runner/toolchain-probe-budget.ts already evaluate these modules at the merge-base (possible homes for a shared symbol; not a statement of ownership):
  packages/platform-apple/src/runner/apple-runner-platform.ts
  packages/platform-apple/src/runner/host.ts
  packages/platform-apple/src/runner/runner-provider.ts
  packages/platform-apple/src/runner/runner-sequence.ts
  packages/platform-apple/src/runner/runner-source.ts
  packages/platform-apple/src/runner/index.ts
  packages/platform-apple/src/runner/runner-cache-metadata.ts

That is the shape #2423 needed: one line pointing at the modules already common to
every affected facade, instead of five people independently reverse-engineering the
same pattern from five separate CI failures. Here there is one added module, so one
block; two independent added modules would print two, each with only its own entries
and homes.

Test plan

  • pnpm vitest run --project unit-core scripts/__tests__/eager-closure-budgets.test.ts — 588 passed, including the added-module listing (with a route), the shared-homes aggregation (two entries sharing one added module, the no-op single-entry case), and this round's two regressions: an equal-size replacement is not growth, so the aggregate stays silent and disjoint growth groups keep their own entries and homes in separate blocks. Both were confirmed non-vacuous by re-introducing the two defects (the added.length > 0 filter, and one merged homes list) and watching each fail.
  • pnpm check:fallow --base origin/main — no issues in 2 changed files; every function in the split grouping is under the complexity thresholds.
  • pnpm typecheck — clean (the earlier --until failure was fix(cli): declare projectConfig on the scroll --until flag #2472's, now on main).
  • pnpm lint — clean.
  • pnpm check:layering — clean.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.54 MB 4.54 MB 0 B
Package (unpacked) 4.54 MB 4.54 MB 0 B
Package (download) 1.35 MB 1.35 MB +9 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.5 ms 26.3 ms -0.2 ms
CLI --help 75.0 ms 73.4 ms -1.6 ms

@thymikee

Copy link
Copy Markdown
Member Author

Needs changes at 78a7a07:

  • The aggregate test filters on added.length > 0, not net growth. Two equal-size or shrinking closures that replace a module with the same new module can now fail, although classifyGrowth passes both. Feed only actual growth into the aggregate and add an equal-size replacement regression.
  • Candidate homes from separate groups are merged into one list labelled as common to every failing entry. Keep each added module and its affected entries together, with a disjoint-groups regression.
  • Fallow fails on describeSharedGrowthHomes (13 cyclomatic / 25 cognitive). Simplify the grouping while addressing the above rather than raising the threshold.

The until typing failures are inherited and fixed by #2472; the Fallow failure is directly related to this change.

@thymikee
thymikee force-pushed the claude/gate-growth-diagnostics branch from 78a7a07 to 0b2a600 Compare September 10, 2026 16:15
…ager closure grows

The no-growth diagnostic in scripts/__tests__/eager-closure-budgets.ts only named the
FIRST newly evaluated module and always advised a dynamic import. On #2423 that sent
five reviewers toward the wrong fix when the growth was a small new module that
belonged in a module every affected entry already evaluated -- the dynamic-import
advice was never coherent for a brand-new module with no old edge to defer.

- describeClosureGrowth now lists every added module (bounded to 10), each with the
  shortest static import route from the entry to it.
- describeSharedGrowthHomes runs once after every entry is evaluated: when two or more
  entries grew by the same added module, it names the modules they already evaluate at
  the merge-base under that module's own package -- candidate homes, not a verdict.
- classifyGrowth's closing advice now states the two common causes (a new static edge,
  or something that used to load lazily) and the two remedies (give the symbol a home
  in a module already in the closure, or make the new edge lazy) instead of prescribing
  one fix.

The verdict logic (when an entry is flagged as having grown) is unchanged.
…ed module

The cross-entry shared-homes note took every entry with a newly evaluated
module, which is not the condition the per-entry rule applies: a closure that
swaps one module for another, or shrinks while adding one, has added modules and
no growth. `classifyGrowth` passes it, so the aggregate must too -- entries now
carry their head closure size and the grouping keeps only the ones whose closure
actually grew.

Candidate homes are no longer unioned across added modules. Each added module
shared by two or more grown entries gets its own block naming those entries with
how much each grew and the merge-base modules exactly those entries evaluate, so
the label no longer claims a home is common to every failing entry when two
independent groups are in play.
@thymikee
thymikee force-pushed the claude/gate-growth-diagnostics branch from f86b2bd to 343fca1 Compare September 10, 2026 17:00
@thymikee

Copy link
Copy Markdown
Member Author

All three addressed in 343fca1 (rebased on main).

1. Aggregate only takes net growth. GrowthForAggregation now carries headClosureSize, and the grouping drops any entry whose head closure is not larger than its merge-base one — the same condition classifyGrowth reports on. A closure that swaps one module for another, or shrinks while adding one, has newly evaluated modules and no growth, so it neither appears in a block nor fails the test. The real-tree test hands over every carried entry and lets the aggregation apply the filter, instead of pre-filtering on added.length > 0.

Regression: an equal-size replacement is not growth, so the aggregate stays silent — two entries, each replacing one module with the same new module at equal closure size (plus a shrinking variant); classifyGrowth is null for both and describeSharedGrowthHomes returns null.

2. One block per added module, no union. sharedGrowthGroups builds a SharedGrowthGroup per added module — the module, the grown entries with their +N, and the merge-base modules exactly those entries evaluate in the added module's own package. Nothing is merged across groups, the cross-group "already claimed as a home" bookkeeping is gone, and the label is now per group: Entries that grew by <module> already evaluate these modules at the merge-base (possible homes for a shared symbol; not a statement of ownership):. Blocks are bounded (5 blocks, 8 entries each, 30 homes each, each with a count of the rest).

Regression: disjoint growth groups keep their own entries and homes in separate blocks — X grew a1/a2, Y grew b1/b2, both added modules in the same package so only the grouping can separate them; asserts two blocks and that neither block carries the other's entries or homes.

Both regressions were confirmed non-vacuous by re-introducing the two defects (the added.length > 0 filter; one merged homes list) and watching each fail.

3. Complexity. pnpm check:fallow --base origin/main: ✓ No issues in 2 changed files. The grouping got smaller rather than larger: the largest function is now sortHomesLeavesFirst (one comparator with two branches); describeSharedGrowthHomes is 6 statements, and the per-group work is split across growthsByAddedModule / sharedHomesForGroup / sharedGrowthGroups / formatGrownEntries / formatCandidateHomes / formatSharedGrowthGroup.

Verification: gate file 588/588 (passes at the merge-base), pnpm typecheck clean (the --until failure was #2472's and is now on main), pnpm lint clean, pnpm check:layering clean. The planted-growth demonstration was re-run on this commit and the PR body's "after" aggregation block refreshed from that run; the plant is discarded.

@thymikee

Copy link
Copy Markdown
Member Author

The three findings are addressed at 343fca1: only net growth contributes, each added module keeps its own candidate homes, and the complexity gate passes. No remaining code findings. The iOS capture-stall failure also occurs on the base and looks unrelated to this diagnostics-only change; resolve or rerun that check before merging.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 10, 2026
@thymikee
thymikee merged commit 220bab0 into main Sep 10, 2026
18 of 19 checks passed
@thymikee
thymikee deleted the claude/gate-growth-diagnostics branch September 10, 2026 18:10
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-10 18:10 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant